<?php
// $Id: Athena.php 27 2010-11-16 12:49:54Z mjohnson60@gmail.com $

/**
 * Enter description here ...
 * 
 * @author Mark Phillip Johnson
 * @version 1.0
 * @copyright Copyright (c) 2010, markjohnson
 * @license http://www.gnu.org/licenses/gpl.html The GNU General Public License v3.0
 *
 */
class Athena extends Object
{
	private static $instance = NULL;
	
	var $headers = array();
	var $html_head = array();
	var $css = array();
	var $javascript = array();
	var $breadcrumbs = array();
	
	var $files = array();
	var $included_files = array();
	
	var $ip_address = '';
	
	var $messages = array();
	
	var $content = '';
	
	/**
	 * Enter description here ...
	 * 
	 * @var Controller
	 */
	var $controller = NULL;

	/**
	 * Enter description here ...
	 */
	public function __construct()
	{
		self::$instance = & $this;
	}

	/**
	 * Enter description here ...
	 * @return Athena
	 */
	public static function &getInstance()
	{
		if (!isset(self::$instance))
		{
			new Athena();
		}
		
		return self::$instance;
	}

	/**
	 * Enter description here ...
	 * @return string
	 */
	public static function getHeaders()
	{
		$_this = & Athena::getInstance();
		return implode("\n", $_this->headers);
	}

	/**
	 * Enter description here ...
	 * @param string $header
	 */
	public static function setHeader($header = NULL)
	{
		$_this = & Athena::getInstance();
		if (!empty($header))
		{
			header($header);
			$_this->headers[] = $header;
		}
	}

	/**
	 * This method is from CodeIgniter
	 * 
	 * Set HTTP Status Header
	 *
	 * @access	public
	 * @param	int 	the status code
	 * @param	string
	 * @return	void
	 */
	public static function httpStatusHeader($code = 200, $text = '')
	{
		page_header();
		
		$stati = array(
			200 => 'OK', 
			201 => 'Created', 
			202 => 'Accepted', 
			203 => 'Non-Authoritative Information', 
			204 => 'No Content', 
			205 => 'Reset Content', 
			206 => 'Partial Content', 
			300 => 'Multiple Choices', 
			301 => 'Moved Permanently', 
			302 => 'Found', 
			304 => 'Not Modified', 
			305 => 'Use Proxy', 
			307 => 'Temporary Redirect', 
			400 => 'Bad Request', 
			401 => 'Unauthorized', 
			403 => 'Forbidden', 
			404 => 'Not Found', 
			405 => 'Method Not Allowed', 
			406 => 'Not Acceptable', 
			407 => 'Proxy Authentication Required', 
			408 => 'Request Timeout', 
			409 => 'Conflict', 
			410 => 'Gone', 
			411 => 'Length Required', 
			412 => 'Precondition Failed', 
			413 => 'Request Entity Too Large', 
			414 => 'Request-URI Too Long', 
			415 => 'Unsupported Media Type', 
			416 => 'Requested Range Not Satisfiable', 
			417 => 'Expectation Failed', 
			500 => 'Internal Server Error', 
			501 => 'Not Implemented', 
			502 => 'Bad Gateway', 
			503 => 'Service Unavailable', 
			504 => 'Gateway Timeout', 
			505 => 'HTTP Version Not Supported'
		);
		
		if ($code == '' or !is_numeric($code))
		{
			Athena::setMessage('Status codes must be numeric', 'error');
		}
		
		if (isset($stati[$code]) and $text == '')
		{
			$text = $stati[$code];
		}
		
		if ($text == '')
		{
			Athena::setMessage('No status text available.  Please check your status code number or supply your own message text.', 'error');
		}
		
		$server_protocol = (isset($_SERVER['SERVER_PROTOCOL'])) ? $_SERVER['SERVER_PROTOCOL'] : FALSE;
		
		if (substr(php_sapi_name(), 0, 3) == 'cgi')
		{
			header("Status: {$code} {$text}", TRUE);
		}
		elseif ($server_protocol == 'HTTP/1.1' or $server_protocol == 'HTTP/1.0')
		{
			header($server_protocol . " {$code} {$text}", TRUE, $code);
		}
		else
		{
			header("HTTP/1.1 {$code} {$text}", TRUE, $code);
		}
	}

	/**
	 * Enter description here ...
	 * @return string
	 */
	public static function getHtmlHead()
	{
		$_this = & Athena::getInstance();
		return implode('', $_this->html_head);
	}

	/**
	 * Enter description here ...
	 * @param string $html_head
	 */
	public static function setHtmlHead($html_head)
	{
		$_this = & Athena::getInstance();
		if (!empty($html_head))
		{
			$_this->html_head[] = $html_head . "\n";
		}
	}

	/**
	 * get_css()
	 *
	 * This function returns the <link> elements that are represented in array form either by the $styles parameter
	 * or returned from the get_css() function
	 *
	 * @param mixed $styles
	 * @return
	 */
	public static function getCss($css = NULL)
	{
		$_this = & Athena::getInstance();
		$output = '';
		if (!isset($css))
		{
			$css = $_this->css;
		}
		
		$preprocess_css = FALSE;
		
		$module_css = '';
		$theme_css = '';
		foreach ($css as $media => $types)
		{
			foreach ($types as $type => $files)
			{
				if ($type == 'module')
				{
					$theme_styles = array();
					foreach (array_keys($css[$media]['theme']) as $theme_style)
					{
						$theme_styles[] = basename($theme_style);
					}
				}
				
				foreach ($types[$type] as $file => $preprocess)
				{
					if (file_exists($file))
					{
						if (!$preprocess || $preprocess_css)
						{
							if ($type == 'module')
							{
								$module_css .= '<link type="text/css" rel="stylesheet" media="' . $media . '" href="' . Config::basePath() . $file . '" />' . "\n";
							}
							else if ($type == 'theme')
							{
								$theme_css .= '<link type="text/css" rel="stylesheet" media="' . $media . '" href="' . Config::basePath() . $file . '" />' . "\n";
							}
							else
							{
								$output . -'<link type="text/css" rel="stylesheet" media="' . $media . '" href="' . Config::basePath() . $file . '" />' . "\n";
							}
						}
					}
				}
			}
		}
		
		return $module_css . $output . $theme_css;
	}

	/**
	 *
	 * @param string $path
	 * @param string $type
	 * @param string $media
	 * @param boolean $preprocess
	 * @return array 
	 */
	public static function addCss($path, $type = 'module', $media = 'all', $preprocess = TRUE)
	{
		$_this = & Athena::getInstance();
		if (isset($path))
		{
			if (!isset($_this->css[$media]))
			{
				$_this->css[$media] = array(
					'module' => array(), 
					'theme' => array()
				);
			}
			
			$_this->css[$media][$type][$path] = $preprocess;
		}
		
		return $_this->css;
	}

	/**
	 * get_js
	 * 
	 * This function outputs the <script> tags that are either inputs or returned by system_add_js()
	 *
	 * @param string $scope
	 * @param mixed $javascript
	 * @return string 
	 */
	public static function getScripts($scope = 'header', $javascript = NULL)
	{
		$_this = & Athena::getInstance();
		if (empty($javascript))
		{
			$javascript = $_this->javascript;
		}
		
		if (empty($javascript))
		{
			return '';
		}
		
		$output = '';
		$embed_prefix = "\n<!--//--><![CDATA[//><!--\n";
		$embed_suffix = "\n//--><!]]>\n";
		
		$files = array();
		
		foreach ($javascript as $type => $script)
		{
			if (!$script)
			{
				continue;
			}
			
			switch ($type)
			{
				case 'setting':
					$output .= '<script type="text/javascript">' . $embed_prefix . '$.extend(Athena.settings, ' . php_to_js(call_user_func_array('array_merge_recursive', $script)) . ');' . $embed_suffix . '</script>' . "\n";
					
					break;
				case 'inline':
					foreach ($script as $inline)
					{
						$output .= '<script type="text/javascript">' . $embed_prefix . $inline . $embed_suffix . '</script>' . "\n";
					}
					
					break;
				default:
					foreach ($script as $path => $info)
					{
						// @TODO: add script caching and preprocessing
						$files[$type] .= '<script type="text/javascript" src="' . Config::basePath() . $path . '"></script>' . "\n";
					}
					
					break;
			}
		}
		
		$output = implode('', $files) . $output;
		
		return $output;
	}

	/**
	 * add_js()
	 *
	 * A global function to add a script to the html
	 *
	 * @param $script
	 * the path to the script or the inline script
	 *
	 * @param $type
	 * @param string $scope
	 * @param boolean $cache
	 * @return array
	 * an array() of the scripts
	 */
	public static function addScripts($script = NULL, $type = 'module', $scope = 'header', $cache = TRUE)
	{
		$_this = & Athena::getInstance();
		if (isset($script))
		{
			// add default js?
			

			if (isset($scope) && !isset($_this->javascript[$scope]))
			{
				$_this->javascript[$scope] = array(
					'core' => array(), 
					'module' => array(), 
					'theme' => array(), 
					'setting' => array(), 
					'inline' => array()
				);
			}
			
			if (isset($type) && isset($scope) && !isset($_this->javascript[$scope][$type]))
			{
				$_this->javascript[$scope][$type] = array();
			}
			
			switch ($type)
			{
				case 'setting':
					$javascript[$scope][$type][] = $script;
					break;
				case 'inline':
					$javascript[$scope][$type][] = array(
						'code' => $script
					);
					break;
				default:
					// If cache is FALSE, don't preprocess the JS file.
					$javascript[$scope][$type][$script] = array(
						'cache' => $cache
					);
			}
		}
		
		if (isset($scope))
		{
			
			if (isset($_this->javascript[$scope]))
			{
				return $_this->javascript[$scope];
			}
			else
			{
				return array();
			}
		}
		else
		{
			return $_this->javascript;
		}
	}

	public static function getBreadcrumbs()
	{
		$_this = & Athena::getInstance();
		return $_this->breadcrumbs;
	}

	/**
	 * system_breadcrumbs()
	 *
	 * sets and gets any breadcrumbs defined by the current page
	 *
	 * @param mixed $breadcrumbs
	 * @return
	 */
	public static function setBreadcrumb($breadcrumb)
	{
		$_this = & Athena::getInstance();
		if (!empty($breadcrumb))
		{
			$_this->breadcrumbs[] = $breadcrumb;
		}
	}

	/**
	 *
	 * @return mixed
	 */
	public static function getMessages()
	{
		$_this = & Athena::getInstance();
		// messages not set when DB connection fails
		return isset($_this->messages) ? $_this->messages : NULL;
	}

	/**
	 *
	 * @param string $message
	 * @param string $type
	 * @param boolean $repeat 
	 */
	public static function setMessage($message = NULL, $type = 'default', $repeat = TRUE)
	{
		$_this = & Athena::getInstance();
		if ($message)
		{
			if (!isset($_this->messages[$type]))
			{
				$_this->messages[$type] = array();
			}
			
			if ($repeat || !in_array($message, $_this->messages[$type]))
			{
				$_this->messages[$type][] = $message;
			}
		}
	}

	/**
	 * 
	 */
	public static function storeMessages()
	{
		$_this = & Athena::getInstance();
		$_SESSION['Athena']['messages'] = $_this->messages;
	}
	
	/**
	 * installSchema()
	 * 
	 * Enter description here ...
	 * 
	 * @param unknown_type $module
	 * @return Ambigous <multitype:, Ambigous, resource, boolean>
	 */
	public static function installSchema($module)
	{
		$installer_class = Renderer::methodForModel($module) . 'Installer';
		
		Moduler::loadInstall($module);
		$schema = Moduler::invokeStatic($installer_class, 'schema');
		
		$retVal = TRUE;
		foreach ($schema as $name => $table)
		{
			$result = Db::query($table);
			
			$retVal = $retVal && ($result !== FALSE);
		}
		
		return $retVal;
	}

	/**
	 *
	 * @param string $module
	 * @return mixed
	 */
	public static function uninstallSchema($module)
	{
		Moduler::loadInstall($module);
		$schema = Moduler::invokeStatic($module, 'schema');
		
		$return = array();
		foreach ($schema as $name => $table)
		{
			$return = Db::dropTable($return, $name);
		}
		
		return $return;
	}

	/**
	 *
	 * @staticvar string $ip_address
	 * @return string
	 */
	public static function ipAddress()
	{
		$_this = & Athena::getInstance();
		if (empty($_this->ip_address))
		{
			$_this->ip_address = $_SERVER['REMOTE_ADDR'];
		}
		
		return $_this->ip_address;
	}

	/**
	 *
	 * @staticvar array $files
	 * @param <type> $type
	 * @param <type> $name
	 * @param <type> $hint
	 * @return string
	 */
	public static function getFilename($type, $name, $hint = NULL)
	{
		$_this = & Athena::getInstance();
		if (!isset($_this->files[$type]))
		{
			$_this->files[$type] = array();
		}
		
		if (isset($hint) && file_exists($hint))
		{
			$_this->files[$type][$name] = $hint;
		}
		else if (isset($_this->files[$type][$name]))
		{
			return $_this->files[$type][$name];
		}
		else if (Db::isActive() && ($file = Db::result(Db::query("SELECT filename FROM system WHERE name = '%s' AND type = '%s'", array(
			$name, 
			$type
		)))))
		{
			$_this->files[$type][$name] = $file;
		}
		else
		{
			$base = base_path();
			$file = $name . '.' . $type;
			
			$possible_files = array(
				$file, 
				$type . 's/' . $file, 
				$type . 's/' . $name . '/' . $file
			);
			
			foreach ($possible_files as $location)
			{
				if (file_exists('./' . $location))
				{
					$_this->files[$type][$name] = $location;
					break;
				}
			}
		}
		
		if (isset($_this->files[$type][$name]))
		{
			return $_this->files[$type][$name];
		}
	}

	/**
	 *
	 * @staticvar array $files
	 * @param <type> $type
	 * @param <type> $name
	 * @return <type>
	 */
	public static function loadFile($type, $name)
	{
		$_this = & Athena::getInstance();
		if (isset($_this->included_files[$type][$name]))
		{
			return TRUE;
		}
		
		$filename = Athena::getFilename($type, $name);
		
		if ($filename)
		{
			include_once ('./' . $filename);
			$_this->included_files[$type][$name] = TRUE;
			
			return TRUE;
		}
		
		return FALSE;
	}

	/**
	 *
	 * @param <type> $type
	 * @param <type> $module
	 * @return <type>
	 */
	public static function getPath($type, $module)
	{
		return dirname(Athena::getFilename($type, $module));
	}

	/**
	 *
	 * @param <type> $mask
	 * @param <type> $directory
	 * @param <type> $key
	 * @param <type> $min_depth
	 * @return <type> 
	 */
	public static function systemListing($mask, $directory, $key = 'name', $min_depth = 1)
	{
		$conf_path = Config::confPath();
		
		$searchdir = array(
			$directory
		);
		$files = array();
		
		$searchdir[] = 'sites/all/' . $directory;
		
		if (file_exists($conf_path . '/' . $directory))
		{
			$searchdir[] = $conf_path . '/' . $directory;
		}
		
		foreach ($searchdir as $dir)
		{
			$files = array_merge($files, Athena::scanDirectory($dir, $mask, array(
				'.', 
				'..' . 'CVS'
			), 0, TRUE, $key, $min_depth));
		}
		
		return $files;
	}

	/**
	 * Finds all files that match a given mask in a given directory.
	 * Directories and files beginning with a period are excluded; this
	 * prevents hidden files and directories (such as SVN working directories)
	 * from being scanned.
	 *
	 * @param $dir
	 * The base directory for the scan, without trailing slash.
	 * @param $mask
	 * The regular expression of the files to find.
	 * @param $nomask
	 * An array of files/directories to ignore.
	 * @param $callback
	 * The callback function to call for each match.
	 * @param $recurse
	 * When TRUE, the directory scan will recurse the entire tree
	 * starting at the provided directory.
	 * @param $key
	 * The key to be used for the returned array of files. Possible
	 * values are "filename", for the path starting with $dir,
	 * "basename", for the basename of the file, and "name" for the name
	 * of the file without an extension.
	 * @param $min_depth
	 * Minimum depth of directories to return files from.
	 * @param $depth
	 * Current depth of recursion. This parameter is only used internally and should not be passed.
	 *
	 * @return
	 * An associative array (keyed on the provided key) of objects with
	 * "path", "basename", and "name" members corresponding to the
	 * matching files.
	 */
	public static function scanDirectory($dir, $mask, $nomask = array('.', '..', 'CVS'), $callback = 0, $recurse = TRUE, $key = 'filename', $min_depth = 0, $depth = 0)
	{
		$key = (in_array($key, array('filename', 'basename', 'name')) ? $key : 'filename');
		$files = array();
		
		if (is_dir($dir) && $handle = opendir($dir))
		{
			while (FALSE !== ($file = readdir($handle)))
			{
				if (!in_array($file, $nomask) && $file[0] != '.')
				{
					if (is_dir("$dir/$file") && $recurse)
					{
						// Give priority to files in this folder by merging them in after any subdirectory files.
						$files = array_merge(Athena::scanDirectory("$dir/$file", $mask, $nomask, $callback, $recurse, $key, $min_depth, $depth + 1), $files);
					}
					elseif ($depth >= $min_depth && preg_match('/' . $mask . '/i', $file) > 0)
					{
						// Always use this match over anything already set in $files with the same $$key.
						$filename = "$dir/$file";
						$basename = basename($file);
						$name = substr($basename, 0, strrpos($basename, '.'));
						$files[$$key] = new stdClass();
						$files[$$key]->filename = $filename;
						$files[$$key]->basename = $basename;
						$files[$$key]->name = $name;
						
						if ($callback)
						{
							$callback($filename);
						}
					}
				}
			}
			
			closedir($handle);
		}
		
		return $files;
	}

	/**
	 * classExists()
	 * 
	 * Enter description here ...
	 * 
	 * @param string $class_name
	 * @param boolean $autoload
	 * @return boolean
	 */
	public static function classExists($class_name, $autoload = FALSE)
	{
		return class_exists($class_name, $autoload);
	}

	/**
	 * Unserializes and appends elements from a serialized string.
	 *
	 * @param $obj
	 *   The object to which the elements are appended.
	 * @param $field
	 *   The attribute of $obj whose value should be unserialized.
	 */
	public static function unpack($obj, $field = 'data')
	{
		if ($obj->$field && $data = unserialize($obj->$field))
		{
			foreach ($data as $key => $value)
			{
				if (!empty($key) && !isset($obj->$key))
				{
					$obj->$key = $value;
				}
			}
		}
		
		return $obj;
	}

	/**
	 * Perform end-of-request tasks.
	 *
	 * In some cases page requests need to end without calling drupal_page_footer().
	 * In these cases, call drupal_exit() instead. There should rarely be a reason
	 * to call exit instead of drupal_exit();
	 *
	 * @param $destination
	 *   If this function is called from drupal_goto(), then this argument
	 *   will be a fully-qualified URL that is the destination of the redirect.
	 *   This should be passed along to hook_exit() implementations.
	 */
	public static function _exit($destination = NULL) 
	{
		if (Initializer::phase() == INITIALIZE_FULL) 
		{
			if (!defined('CONFIGURATION_MODE') || CONFIGURATION_MODE != 'UPDATE') 
			{
				Moduler::invokeAll('exit', $destination);
			}
			
			session_write_close();
		}
		
		exit();
	}

	/**
	 * Wrapper around parse_url() to parse a system URL string into an associative array, suitable for url().
	 *
	 * This function should only be used for URLs that have been generated by the
	 * system, resp. url(). It should not be used for URLs that come from external
	 * sources, or URLs that link to external resources.
	 *
	 * The returned array contains a 'path' that may be passed separately to url().
	 * For example:
	 * @code
	 * $options = drupal_parse_url($_GET['destination']);
	 * $my_url = url($options['path'], $options);
	 * $my_link = l('Example link', $options['path'], $options);
	 * @endcode
	 *
	 * This is required, because url() does not support relative URLs containing a
	 * query string or fragment in its $path argument. Instead, any query string
	 * needs to be parsed into an associative query parameter array in
	 * $options['query'] and the fragment into $options['fragment'].
	 *
	 * @param $url
	 * The URL string to parse, f.e. $_GET['destination'].
	 *
	 * @return
	 * An associative array containing the keys:
	 * - 'path': The path of the URL. If the given $url is external, this includes
	 * the scheme and host.
	 * - 'query': An array of query parameters of $url, if existent.
	 * - 'fragment': The fragment of $url, if existent.
	 *
	 * @see url()
	 * @see drupal_goto()
	 * @ingroup php_wrappers
	 */
	public static function parseUrl($url)
	{
		$options = array(
			'path' => NULL, 
			'query' => array(), 
			'fragment' => ''
		);
		
		// External URLs: not using parse_url() here, so we do not have to rebuild
		// the scheme, host, and path without having any use for it.
		if (strpos($url, '://') !== FALSE)
		{
			// Split off everything before the query string into 'path'.
			$parts = explode('?', $url);
			$options['path'] = $parts[0];
			
			// If there is a query string, transform it into keyed query parameters.
			if (isset($parts[1]))
			{
				$query_parts = explode('#', $parts[1]);
				parse_str($query_parts[0], $options['query']);
				
				// Take over the fragment, if there is any.
				if (isset($query_parts[1]))
				{
					$options['fragment'] = $query_parts[1];
				}
			}
		}
		else
		{
			// parse_url() does not support relative URLs, so make it absolute. E.g. the
			// relative URL "foo/bar:1" isn't properly parsed.
			$parts = parse_url('http://example.com/' . $url);
			// Strip the leading slash that was just added.
			$options['path'] = substr($parts['path'], 1);
			
			if (isset($parts['query']))
			{
				parse_str($parts['query'], $options['query']);
			}
			
			if (isset($parts['fragment']))
			{
				$options['fragment'] = $parts['fragment'];
			}
		}
		
		// The 'q' parameter contains the path of the current page if clean URLs are
		// disabled. It overrides the 'path' of the URL when present, even if clean
		// URLs are enabled, due to how Apache rewriting rules work.
		if (isset($options['query']['q']))
		{
			$options['path'] = $options['query']['q'];
			unset($options['query']['q']);
		}
		
		return $options;
	}

	/**
	 * redirect()
	 *
	 * This function redirects a user to a different location.
	 *
	 * This is useful for form validation and direction, 404 errors, etc.
	 *
	 * @param mixed $url
	 * @param integer $http_code
	 * @return void
	 */
	public static function redirect($url, array $options = array(), $http_response_code = 302)
	{
		if (isset($_GET['destination']) && !url_is_external($_GET['destination']))
		{
			$destination = Athena::parseUrl($_GET['destination']);
			$path = $destination['path'];
			$options['query'] = $destination['query'];
			$options['fragment'] = $destination['fragment'];
		}
		
		Moduler::alter('redirect', $url, $options, $http_response_code);
		
		// The 'Location' HTTP header must be absolute.
		$options['absolute'] = TRUE;
		
		$url = url($path, $options);
		
		header('Location: ' . $url, TRUE, $http_response_code);
		Athena::_exit($url);
	}

	/**
	 * Return data in JSON format.
	 *
	 * This function should be used for JavaScript callback functions returning
	 * data in JSON format. It sets the header for JavaScript output.
	 *
	 * @param $var
	 * (optional) If set, the variable will be converted to JSON and output.
	 */
	public static function json($var = NULL)
	{
		// We are returning JavaScript, so tell the browser.
		Athena::setHeader('Content-Type: text/javascript; charset=utf-8');
		
		if (isset($var))
		{
			echo(Athena::js($var));
		}
	}

	/**
	 * Converts a PHP variable into its Javascript equivalent.
	 *
	 * We use HTML-safe strings, i.e. with <, > and & escaped.
	 */
	public static function js($var)
	{
		switch (gettype($var))
		{
			case 'boolean':
				return $var ? 'true' : 'false'; // Lowercase necessary!
			case 'integer':
			case 'double':
				return $var;
			case 'resource':
			case 'string':
				return '"' . str_replace(array("\r", "\n", "<", ">", "&"),
                              array('\r', '\n', '\x3c', '\x3e', '\x26'),
                              addslashes($var)) .'"';
			case 'array':
				// Arrays in JSON can't be associative. If the array is empty or if it
				// has sequential whole number keys starting with 0, it's not associative
				// so we can go ahead and convert it as an array.
				if (empty($var) || array_keys($var) === range(0, sizeof($var) - 1))
				{
					$output = array();
					foreach ($var as $v)
					{
						$output[] = Athena::js($v);
					}
					return '[ ' . implode(', ', $output) . ' ]';
				}
			// Otherwise, fall through to convert the array as an object.
			case 'object':
				$output = array();
				foreach ($var as $k => $v)
				{
					$output[] = Athena::js(strval($k)) . ': ' . Athena::js($v);
				}
				return '{ ' . implode(', ', $output) . ' }';
			default:
				return 'null';
		}
	}
}
